﻿using BaseData.DomainLib.AggregatesModel.CalculateUnitAggregate;
using BaseData.DomainLib.AggregatesModel.CustomerAggregate;
using BaseData.DomainLib.AggregatesModel.MaterialAggregate;
using BaseData.DomainLib.AggregatesModel.MaterialCategoryAggregate;
using BaseData.DomainLib.AggregatesModel.SaleOrderAggregate;
using BaseData.DomainLib.AggregatesModel.SupplierAggregate;
using BaseData.DomainLib.AggregatesModel.WarehouseAggregate;
using BaseData.DomainLib.AggregatesModel.WorkstationAggregate;
using BaseData.InfrastructureLib.EntityConfigurations;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Swimj.ApplicationLib.Entities;
using Swimj.Core.Providers;
using Swimj.Core.Securities;
using Swimj.DomainLib.SeedWork;
using Swimj.InfrastructureLib.Extensions;
using System.Data;
using System.Diagnostics.CodeAnalysis;

namespace BaseData.InfrastructureLib
{
    public class BaseDataDbContext : DbContext, IUnitOfWork
    {
        private readonly ISecurityUser _securityUser;
        private readonly IDateTimeProvider _dateTimeProvider;
        private readonly IMediator? _mediator;
        private IDbContextTransaction? _currentTransaction;

        [NotNull] public DbSet<MaterialCategory>? MaterialCategories { get; set; }

        [NotNull] public DbSet<Material>? Materials { get; set; }

        [NotNull] public DbSet<CalculateUnit>? Units { get; set; }

        [NotNull] public DbSet<Customer>? Customers { get; set; }

        [NotNull] public DbSet<Supplier>? Suppliers { get; set; }
        
        [NotNull] public DbSet<Workstation>? Workstations { get; set; }
        
        [NotNull] public DbSet<SaleOrder>? SaleOrders { get; set; }

        [NotNull] public DbSet<Warehouse>? Warehouses { get; set; }

        public IDbContextTransaction? GetCurrentTransaction() => _currentTransaction;

        public bool HasActiveTransaction => _currentTransaction != null;
       

        public BaseDataDbContext(DbContextOptions<BaseDataDbContext> options, IMediator mediator,
            ISecurityUser securityUser, IDateTimeProvider dateTimeProvider) :
            base(options)
        {
            _mediator = mediator;
            _securityUser = securityUser;
            _dateTimeProvider = dateTimeProvider;

            System.Diagnostics.Debug.WriteLine("ProductionDbContext::ctor ->" + this.GetHashCode());
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new MaterialCategoryEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new MaterialEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new CalculateUnitEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new CustomerEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new SupplierEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new WorkstationEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new SaleOrderEntityTypeConfiguration());
            modelBuilder.ApplyConfiguration(new SaleOrderDetailEntityTypeConfiguration());
        }

        public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
        {
            // Dispatch Domain Events collection. 
            // Choices:
            // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including  
            // side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime
            // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. 
            // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. 

            if (_mediator != null)
                await _mediator.DispatchDomainEventsAsync(this);

            // After executing this line all the changes (from the Command Handler and Domain Event Handlers) 
            // performed through the DbContext will be committed
            var result = await base.SaveChangesAsync(cancellationToken);

            return true;
        }

        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
        {
            foreach (var entry in ChangeTracker.Entries<IAuditableEntity>().ToList())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.Entity.CreatedOn = _dateTimeProvider.GetCurrentDateTime();
                        entry.Entity.CreatedBy = _securityUser.GetUserId()?.ToString();
                        break;

                    case EntityState.Modified:
                        entry.Entity.LastModifiedOn = _dateTimeProvider.GetCurrentDateTime();
                        entry.Entity.LastModifiedBy = _securityUser.GetUserId()?.ToString();
                        break;
                }
            }

            return await base.SaveChangesAsync(cancellationToken);
        }

        public async Task<IDbContextTransaction?> BeginTransactionAsync()
        {
            if (_currentTransaction != null) return null;

            _currentTransaction = await Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);

            return _currentTransaction;
        }

        public async Task CommitTransactionAsync(IDbContextTransaction transaction)
        {
            if (transaction == null) throw new ArgumentNullException(nameof(transaction));
            if (transaction != _currentTransaction)
                throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");

            try
            {
                await SaveChangesAsync();
                await transaction.CommitAsync();
            }
            catch
            {
                RollbackTransaction();
                throw;
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }

        public void RollbackTransaction()
        {
            try
            {
                _currentTransaction?.Rollback();
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }
    }
}